// Copyright (c) 2025 Hemashushu <hippospark@gmail.com>, All rights reserved.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License version 2.0 and additional exceptions.
// For more details, see the LICENSE, LICENSE.additional, and CONTRIBUTING files.

use crate::transition::Transition;

pub const MAIN_ROUTE_INDEX: usize = 0;

// Object file structure:
//
// ```diagram
// |-- main route --\
// |                |-- node 0 --\
// |                |            |- transition item --> points to the next node
// |                |            |- transition item --> points to the next node
// |                |            |- ...
// |                |            \- transition item --> points to the next node
// |                |-- node 1
// |                |-- ...
// |                |-- node N
// |
// |-- sub route (generated by lookaround assertions)
// |-- sub route (generated by lookaround assertions)
// ```

// The result of compilation.
//
// One regular expression generates one object file.
#[derive(Debug)]
pub struct ObjectFile {
    // Generally, one regular expression consists of a series of nodes, forming a "route."
    // However, "lookahead assertions" or "lookbehind assertions" in the regular expression
    // may generate sub-routes. Therefore, one object file can contain multiple "routes,"
    // where the first route is the main route, and others are sub-routes.
    pub routes: Vec<Route>,

    // The names of capture groups.
    pub capture_group_names: Vec<Option<String>>,
}

// A series of nodes.
#[derive(Debug)]
pub struct Route {
    // A route consists of multiple nodes.
    // Each node contains multiple transitions (or character patterns),
    // which are conceptually similar to functions in programming languages.
    pub nodes: Vec<Node>,

    // The index of the entry node in the node list.
    pub start_node_index: usize,

    // The index of the exit node in the node list.
    pub end_node_index: usize,

    // True if the expression starts with `^`, meaning that if a route match fails,
    // the process will not move forward by one character to try matching again.
    // This is also true for "lookahead assertions" and "lookbehind assertions."
    pub is_fixed_start_position: bool,
}

// A route consists of multiple nodes.
// Each node has one or more transitions,
// except for the exit node, which has no transitions.
#[derive(Debug)]
pub struct Node {
    pub transition_items: Vec<TransitionItem>,
}

// Transition items within a node.
#[derive(Debug)]
pub struct TransitionItem {
    // The transition object.
    pub transition: Transition,

    // The index of the next node to go to when the transition executes successfully.
    pub target_node_index: usize,
}

impl ObjectFile {
    pub fn new() -> Self {
        ObjectFile {
            routes: vec![],
            capture_group_names: vec![],
        }
    }

    // Creates a route object and returns its index.
    pub fn create_route(&mut self) -> usize {
        let route = Route {
            nodes: vec![],
            start_node_index: 0,
            end_node_index: 0,
            is_fixed_start_position: false,
        };

        let idx = self.routes.len();
        self.routes.push(route);
        idx
    }

    // Creates a capture group object and returns its index.
    pub fn create_capture_group(&mut self, name: Option<String>) -> usize {
        let idx = self.capture_group_names.len();
        self.capture_group_names.push(name);
        idx
    }

    // Retrieves the index of a capture group by its name.
    pub fn get_capture_group_index_by_name(&self, name: &str) -> Option<usize> {
        self.capture_group_names.iter().position(|item| match item {
            Some(n) => n == name,
            None => false,
        })
    }

    // Retrieves the name of a capture group by its index.
    pub fn get_capture_group_name_by_index(&self, index: usize) -> Option<&str> {
        let opt_name = &self.capture_group_names[index];
        if let Some(name) = opt_name {
            Some(name.as_str())
        } else {
            None
        }
    }

    // For debugging: generates a textual representation of the object file.
    pub fn get_debug_text(&self) -> String {
        let mut ss = vec![];

        // Routes
        if self.routes.len() == 1 {
            ss.push(self.routes[0].get_debug_text());
        } else {
            for (route_index, route) in self.routes.iter().enumerate() {
                ss.push(format!("= ${}", route_index));
                ss.push(route.get_debug_text());
            }
        }

        // Capture groups
        for (capture_group_index, opt_capture_group_name) in
            self.capture_group_names.iter().enumerate()
        {
            let s = if let Some(name) = &opt_capture_group_name {
                format!("# {{{}}}, {}", capture_group_index, name)
            } else {
                format!("# {{{}}}", capture_group_index)
            };
            ss.push(s);
        }

        ss.join("\n")
    }
}

impl Default for ObjectFile {
    fn default() -> Self {
        Self::new()
    }
}

impl Route {
    // Creates a node object and returns its index.
    pub fn create_node(&mut self) -> usize {
        let node = Node {
            transition_items: vec![],
        };
        let idx = self.nodes.len();
        self.nodes.push(node);

        idx
    }

    // Creates a transition item and returns its index.
    pub fn create_transition_item(
        &mut self,
        source_node_index: usize,
        target_node_index: usize,
        transition: Transition,
    ) -> usize {
        let transition_item = TransitionItem {
            transition,
            target_node_index,
        };

        let idx = self.nodes[source_node_index].transition_items.len();

        self.nodes[source_node_index]
            .transition_items
            .push(transition_item);

        idx
    }

    // For debugging: generates a textual representation of the route.
    pub fn get_debug_text(&self) -> String {
        let mut ss = vec![];

        for (node_index, node) in self.nodes.iter().enumerate() {
            // Node
            let prefix = if node_index == self.start_node_index {
                '>'
            } else if node_index == self.end_node_index {
                '<'
            } else {
                '-'
            };

            let s = format!("{} {}", prefix, node_index);
            ss.push(s);

            // Transition items
            for transition_item in &node.transition_items {
                let s = format!(
                    "  -> {}, {}",
                    transition_item.target_node_index, transition_item.transition
                );
                ss.push(s);
            }
        }

        ss.join("\n")
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::{assert_eq, assert_str_eq};

    use crate::{
        object_file::ObjectFile,
        transition::{CharTransition, Transition},
    };

    #[test]
    fn test_object_file_create_route() {
        let mut object_file = ObjectFile::new();

        // Create a route
        {
            let route_index = object_file.create_route();
            let route = &mut object_file.routes[route_index];

            // Create a node
            let _idx0 = route.create_node();

            assert_str_eq!(
                route.get_debug_text(),
                "\
> 0"
            );

            // Create other nodes
            let idx1 = route.create_node();
            let _idx2 = route.create_node();
            let idx3 = route.create_node();

            route.start_node_index = idx1;
            route.end_node_index = idx3;

            assert_str_eq!(
                route.get_debug_text(),
                "\
- 0
> 1
- 2
< 3"
            );
        }

        // Create another route
        {
            let route_index = object_file.create_route();
            let route = &mut object_file.routes[route_index];

            // Create a node
            let _idx0 = route.create_node();

            assert_str_eq!(
                object_file.get_debug_text(),
                "\
= $0
- 0
> 1
- 2
< 3
= $1
> 0"
            );
        }
    }

    #[test]
    fn test_object_file_create_capture_group() {
        let mut object_file = ObjectFile::new();
        let route_index = object_file.create_route();
        let route = &mut object_file.routes[route_index];

        route.create_node();
        route.create_node();

        object_file.create_capture_group(None);
        object_file.create_capture_group(Some("foo".to_owned()));
        object_file.create_capture_group(None);

        assert_str_eq!(
            object_file.get_debug_text(),
            "\
> 0
- 1
# {0}
# {1}, foo
# {2}"
        );

        assert_eq!(object_file.get_capture_group_index_by_name("foo"), Some(1));
        assert!(object_file.get_capture_group_index_by_name("bar").is_none());
    }

    #[test]
    fn test_route_append_transition() {
        let mut object_file = ObjectFile::new();
        let route_index = object_file.create_route();
        let route = &mut object_file.routes[route_index];

        let node_idx0 = route.create_node();
        let node_idx1 = route.create_node();
        let node_idx2 = route.create_node();
        let node_idx3 = route.create_node();

        let trans_idx0 = route.create_transition_item(
            node_idx0,
            node_idx1,
            Transition::Char(CharTransition::new('a')),
        );

        assert_str_eq!(
            route.get_debug_text(),
            "\
> 0
  -> 1, Char 'a'
- 1
- 2
- 3"
        );

        assert_eq!(trans_idx0, 0);

        let trans_idx1 = route.create_transition_item(
            node_idx0,
            node_idx2,
            Transition::Char(CharTransition::new('b')),
        );

        let trans_idx2 = route.create_transition_item(
            node_idx0,
            node_idx3,
            Transition::Char(CharTransition::new('c')),
        );

        assert_str_eq!(
            route.get_debug_text(),
            "\
> 0
  -> 1, Char 'a'
  -> 2, Char 'b'
  -> 3, Char 'c'
- 1
- 2
- 3"
        );

        assert_eq!(trans_idx1, 1);
        assert_eq!(trans_idx2, 2);

        let trans_idx3 = route.create_transition_item(
            node_idx1,
            node_idx2,
            Transition::Char(CharTransition::new('x')),
        );

        assert_str_eq!(
            route.get_debug_text(),
            "\
> 0
  -> 1, Char 'a'
  -> 2, Char 'b'
  -> 3, Char 'c'
- 1
  -> 2, Char 'x'
- 2
- 3"
        );

        assert_eq!(trans_idx3, 0);
    }
}
